package compiler.scanner;

import java.io.IOException;
import java.io.Reader;
import java.util.Observable;

public class Scanner
    extends Observable
{
    private String IDENTIFIER_REGEX_PATTERN = "[a-zA-Z0-9_]";

    private Reader inputReader;
    private SetOfSymbols setOfSymbols = new SetOfSymbols();
    private SetOfKeywords setOfKeywords = new SetOfKeywords();

    private Character currentChar;
    private Character nextChar;
    private boolean isEof = false;
    private int currentLine = 1;
    private int currentCol = -1;


    private Token nextToken = null;

    public Scanner(Reader reader)
    {
        inputReader = reader;
        nextChar = currentChar = ' ';
    }

    public Token read()
    {
        if (nextToken == null)
        {
            nextToken = getNextToken();
        }
        Token token = nextToken;
        nextToken = getNextToken();
        return token;
    }

    public Token peek()
    {
        return nextToken;
    }

    public int getCurrentLine()
    {
        return currentLine;
    }

    public String getTokenAsString(TokenId tokenId)
    {
        String tokenStr = "";
        Character tokenCh = setOfSymbols.GetSymbol(tokenId);
        if (tokenCh != null)
        {
            tokenStr = tokenCh.toString();
        }
        String keyword = setOfKeywords.GetKeyword(tokenId);
        if (keyword != null)
        {
            tokenStr = keyword;
        }
        return tokenStr;
    }

    public int getCurrentCol()
    {
        return currentCol;
    }

    public Token getNextToken()
    {
        Token token = null;
        while (token == null)
        {
            if (isEof)
            {
                return new Token(TokenId.EOF);
            }

            if (Character.isWhitespace(currentChar))
            {
                skipWhitespaces();
            }
            else if (Character.isAlphabetic(currentChar))
            {
                token = readAlphabeticToken();
            }
            else if (Character.isDigit(currentChar))
            {
                token = readNumericToken();
            }
            else if (setOfSymbols.containsKey(currentChar))
            {
                token = readSymbolicToken();
            }
            else if (currentChar == '&')
            {
                token = readLogicalAndToken();
            }
            else if (currentChar == '/')
            {
                token = readTokenFromSlash();
            }
            else if (currentChar == '|')
            {
                token = ReadLogicalOrToken();
            }
            else
            {
                processError();
            }
        }
        return token;
    }

    private Token readSymbolicToken()
    {
        Token token = new Token(setOfSymbols.get(currentChar));
        readChar();
        return token;
    }

    private Token readAlphabeticToken()
    {
        String word = readWord();
        TokenId tokenId = (setOfKeywords.containsKey(word)) ? setOfKeywords.get(word) : TokenId.IDENTIFIER;

        return new Token(tokenId, word);
    }

    private Token readNumericToken()
    {
        Token token = null;
        String numericString = readNumericString();
        if (!numericString.isEmpty())
        {
            try
            {
                int tokenData = Integer.parseInt(numericString);
                token = new Token(TokenId.INTEGER_LITERAL, tokenData);
            }
            catch (NumberFormatException ex)
            {
                setChanged();
                notifyObservers("Scan error(" + currentLine + ", " + currentCol + "): Numeric string too long: " + numericString);
            }
        }
        return token;
    }

    private Token readLogicalAndToken()
    {
        Token token = null;
        if (nextChar == '&')
        {
            token = new Token(TokenId.AND);
            readChar();
            readChar();
        }
        else
        {
            processError('&');
        }
        return token;
    }

    private Token ReadLogicalOrToken()
    {
        Token token = null;
        if (nextChar == '|')
        {
            token = new Token(TokenId.OR);
            readChar();
            readChar();
        }
        else
        {
            processError('|');
        }
        return token;
    }

    private Token readTokenFromSlash()
    {
        Token token = null;
        if (nextChar == '/')
        {
            String data = readSingleLineComments();
            token = new Token(TokenId.COMMENT, data);
        }
        else if (nextChar == '*')
        {
            String data = readMultiLineComments();
            token = new Token(TokenId.COMMENT, data);
        }
        else
        {
            token = new Token(TokenId.DIV);
            readChar();
        }
        return token;
    }

    private void readChar()
    {
        try
        {
            currentChar = nextChar;
            if (currentChar != Character.MAX_VALUE)
            {
                nextChar = readNextChar();
            }
            else
            {
                isEof = true;
            }
        }
        catch (IOException ex)
        {
            System.out.println(ex.getMessage());
            isEof = true;
        }
    }

    private Character readNextChar()
        throws IOException
    {
        Character newChar = Character.MAX_VALUE;
        int ch = inputReader.read();
        increaseCurrentCaretPosition();
        if (ch != -1)
        {
            newChar = (char) ch;
        }
        return newChar;
    }

    private void increaseCurrentCaretPosition()
    {
        currentCol++;
        if (currentChar == Character.LINE_SEPARATOR)
        {
            currentLine++;
            currentCol = -1;
        }
    }

    private void processError(Character unexpectedChar)
    {
        setChanged();
        notifyObservers("Scan error(" + currentLine + ", " + currentCol + "): Unexpected symbol " + unexpectedChar);
        readChar();
    }

    private void processError()
    {
        processError(currentChar);
    }

    private String readNumericString()
    {
        String numericString = "";
        while (Character.isDigit(currentChar))
        {
            numericString += currentChar;
            readChar();
        }
        if (Character.isAlphabetic(currentChar))
        {
            processError();
            numericString = "";
        }
        return numericString;
    }

    private String readWord()
    {
        String word = "";
        while (currentChar.toString().matches(IDENTIFIER_REGEX_PATTERN))
        {
            word += currentChar;
            readChar();
        }
        return word;
    }

    private String readSingleLineComments()
    {
        String commentString = "";
        while (currentChar != Character.LINE_SEPARATOR && !isEof)
        {
            commentString += currentChar;
            readChar();
        }
        return commentString;
    }

    private String readMultiLineComments()
    {
        String commentString = "";
        while (!(currentChar == '*' && nextChar == '/') && !isEof)
        {
            commentString += currentChar;
            readChar();
        }
        commentString += "*/";
        readChar();
        readChar();
        return commentString;
    }

    private void skipWhitespaces()
    {
        while (Character.isWhitespace(currentChar))
        {
            readChar();
        }
    }

}
